package org.elasticsearch.rest;

import org.elasticsearch.common.CheckedConsumer;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.apache.lucene.search.spell.LevensteinDistance;
import org.apache.lucene.util.CollectionUtil;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

public abstract class BaseRestHandler extends AbstractComponent implements RestHandler {

    public static final Setting<Boolean> MULTI_ALLOW_EXPLICIT_INDEX =
            Setting.boolSetting("rest.action.multi.allow_explicit_index", true, Setting.Property.NodeScope);

    protected BaseRestHandler(Settings settings) {
        super(settings);
    }

    @Override
    public final void handleRequest(RestRequest request, RestChannel channel) throws Exception {
        // prepare the request for execution; has the side effect of touching the request parameters
        final RestChannelConsumer action = prepareRequest(request);

        // validate unconsumed params, but we must exclude params used to format the response
        // use a sorted set so the unconsumed parameters appear in a reliable sorted order
        final SortedSet<String> unconsumedParams =
                request.unconsumedParams().stream().filter(p -> !responseParams().contains(p)).collect(Collectors.toCollection(TreeSet::new));

        // validate the non-response params
        if (!unconsumedParams.isEmpty()) {
            final Set<String> candidateParams = new HashSet<>();
            candidateParams.addAll(request.consumedParams());
            candidateParams.addAll(responseParams());
            throw new IllegalArgumentException(unrecognized(request, unconsumedParams, candidateParams, "parameter"));
        }

        // execute the action
        action.accept(channel);
    }

    protected final String unrecognized(
            final RestRequest request,
            final Set<String> invalids,
            final Set<String> candidates,
            final String detail) {
        String message = String.format(
                Locale.ROOT,
                "request [%s] contains unrecognized %s%s: ",
                request.path(),
                detail,
                invalids.size() > 1 ? "s" : "");
        boolean first = true;
        for (final String invalid : invalids) {
            final LevensteinDistance ld = new LevensteinDistance();
            final List<Tuple<Float, String>> scoredParams = new ArrayList<>();
            for (final String candidate : candidates) {
                final float distance = ld.getDistance(invalid, candidate);
                if (distance > 0.5f) {
                    scoredParams.add(new Tuple<>(distance, candidate));
                }
            }
            CollectionUtil.timSort(scoredParams, (a, b) -> {
                // sort by distance in reverse order, then parameter name for equal distances
                int compare = a.v1().compareTo(b.v1());
                if (compare != 0) return -compare;
                else return a.v2().compareTo(b.v2());
            });
            if (first == false) {
                message += ", ";
            }
            message += "[" + invalid + "]";
            final List<String> keys = scoredParams.stream().map(Tuple::v2).collect(Collectors.toList());
            if (keys.isEmpty() == false) {
                message += " -> did you mean " + (keys.size() == 1 ? "[" + keys.get(0) + "]" : "any of " + keys.toString()) + "?";
            }
            first = false;
        }

        return message;
    }

    /**
     * REST requests are handled by preparing a channel consumer that represents the execution of
     * the request against a channel.
     */
    @FunctionalInterface
    protected interface RestChannelConsumer extends CheckedConsumer<RestChannel, Exception> {
    }


    protected abstract RestChannelConsumer prepareRequest(RestRequest request) throws IOException;


    protected Set<String> responseParams() {
        return Collections.emptySet();
    }

}
